iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
佛心分享-IT 人自學之術

出發吧!後端菜鳥:30 天的後端學習紀錄系列 第 6

Day 06|CRUD 集合!完成第一組 RESTful API

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260919/201694970d2YpcLuBL.jpg

前言

前面我們已經使用 Express 建立 Server,也知道可以透過 app.get() 建立 API,不過實際開發一個功能時,通常不會只有「取得資料」這件事。

以筆記系統為例,使用者除了查看筆記之外,還會需要新增筆記、修改內容,甚至刪除不需要的筆記,而這些常見的資料操作,就可以整理成 CRUD。

這一篇我們會以 Notes 筆記功能為例,一次建立一組完整的 CRUD API,並認識 CRUD 與 HTTP Method 的關係、RESTful API 的基本設計,以及 Express 中很常使用的 req.paramsreq.bodyexpress.json()

這次會完成以下 API:

GET    /notes
GET    /notes/:id
POST   /notes
PATCH  /notes/:id
DELETE /notes/:id

目前還不會使用資料庫,而是先用 JavaScript 陣列暫時存放資料,讓我們把注意力放在 CRUD 和 API 的操作流程上。

CRUD 是什麼?

CRUD 是四種常見資料操作的縮寫,分別是 Create、Read、Update 和 Delete,也就是建立、讀取、更新與刪除資料。

CRUD 全名 用途
C Create 建立資料
R Read 讀取資料
U Update 更新資料
D Delete 刪除資料

以筆記系統來說,新增一篇筆記就是 Create,查看筆記是 Read,修改筆記內容是 Update,而刪除筆記則是 Delete。雖然不同網站提供的功能很多,但只要涉及資料操作,經常都能從 CRUD 的角度理解。

CRUD 與 HTTP Method 的關係

CRUD 描述的是「想對資料做什麼」,而當前端透過 HTTP 向後端發送 Request 時,通常會搭配不同的 HTTP Method 表達這次操作的目的。

常見的對應方式如下:

CRUD HTTP Method 用途
Create POST 建立資料
Read GET 取得資料
Update PATCH / PUT 修改資料
Delete DELETE 刪除資料

例如 Notes API 可以設計成:

GET    /notes
GET    /notes/:id
POST   /notes
PATCH  /notes/:id
DELETE /notes/:id

雖然這幾支 API 都圍繞著 /notes,但因為搭配了不同的 HTTP Method,所以代表的操作也不同。GET /notes 是取得筆記,POST /notes 是建立筆記,而 DELETE /notes/:id 則是刪除某一篇筆記。

RESTful API 基本設計

RESTful API 是常見的 API 設計方式之一,其中一個重要概念,是讓 URL 表達「資源」,再透過 HTTP Method 表達「要對資源進行什麼操作」。

例如目前操作的資源是筆記,因此 URL 可以使用:

/notes

而不是另外設計成:

/getNotes
/createNote
/updateNote
/deleteNote

因為取得、建立、修改和刪除這些動作,已經可以交給 HTTP Method 表達。

例如:

GET /notes
POST /notes

兩者使用相同的 /notes,但前者表示取得 Notes,後者表示建立新的 Note。

如果要操作特定的一筆資料,可以把資料的 ID 放進 URL,例如:

GET /notes/1
PATCH /notes/1
DELETE /notes/1

在 Express 中,我們可以把這個 ID 寫成動態參數:

/notes/:id

其中 :id 代表這個位置不是固定文字,而是由 Client 傳入的實際值。

建立暫存 Notes 資料

正式的後端專案通常會把資料存放在資料庫中,不過目前我們還沒有進入資料庫,因此先使用 JavaScript 陣列模擬資料。

先建立基本的 Express Server:

const express = require("express");

const app = express();

app.use(express.json());

app.listen(3000, function () {
  console.log("Server running at http://localhost:3000");
});

接著建立暫時使用的 Notes:

let notes = [
  {
    id: 1,
    title: "學習 Express",
    content: "今天開始學習 Express CRUD"
  },
  {
    id: 2,
    title: "學習 RESTful API",
    content: "理解 HTTP Method 和 API 的關係"
  }
];

目前可以先把這個 notes 陣列想像成一個非常簡單的資料庫,接下來的新增、讀取、修改和刪除都會直接操作這個陣列。

不過這些資料只存在程式執行期間,只要 Server 重新啟動,陣列就會重新建立,剛剛新增或修改的內容也會消失。這也是之後為什麼需要資料庫的原因之一。

建立 GET /notes

首先從最簡單的 Read 開始。如果 Client 想取得目前所有筆記,可以建立 GET /notes

app.get("/notes", function (req, res) {
  res.status(200).json({
    data: notes
  });
});

當 Client 發送下面這個 Request:

GET /notes

Server 就會回傳目前所有 Notes:

{
  "data": [
    {
      "id": 1,
      "title": "學習 Express",
      "content": "今天開始學習 Express CRUD"
    },
    {
      "id": 2,
      "title": "學習 RESTful API",
      "content": "理解 HTTP Method 和 API 的關係"
    }
  ]
}

這裡使用 res.status(200) 設定 HTTP Status Code。200 OK 表示這次 Request 已經成功處理,再透過 json() 將資料以 JSON 格式回傳給 Client。

建立 GET /notes/:id

除了取得所有筆記之外,實際網站通常也會需要取得其中一篇筆記。例如 Client 發送:

GET /notes/1

就代表想取得 ID 為 1 的 Note。

在 Express 中,可以把這種會改變的 URL 片段設計成 Route Parameter,例如:

app.get("/notes/:id", function (req, res) {
  const id = Number(req.params.id);

  const note = notes.find(function (note) {
    return note.id === id;
  });

  if (!note) {
    return res.status(404).json({
      message: "Note not found"
    });
  }

  res.status(200).json({
    data: note
  });
});

這裡的 :id 就是 Route Parameter,可以把它理解成 URL 中的動態參數。當 Client 發送:

GET /notes/2

Express 會把 2 這個值放進 req.params

req.params 是一個物件,專門用來存放 Route 中的動態參數。例如可以先印出:

console.log(req.params);

會得到:

{
  id: "2"
}

因此可以透過:

req.params.id

取得 "2"

需要注意的是,從 URL 取得的參數預設會是字串,但目前 notes 裡的 id 是 Number,所以這裡先使用 Number() 轉型:

const id = Number(req.params.id);

接著再使用 find(),從 notes 陣列中尋找相同 ID 的資料:

const note = notes.find(function (note) {
  return note.id === id;
});

如果找到資料,就回傳該篇 Note;如果找不到,find() 會回傳 undefined,因此可以透過 if (!note) 判斷,並回傳 404 Not Found

if (!note) {
  return res.status(404).json({
    message: "Note not found"
  });
}

這樣一來,GET /notes/:id 就可以根據 URL 中不同的 ID,取得對應的 Note。

建立 POST /notes

接下來實作 Create。如果 Client 想建立新的 Note,可以使用 POST /notes,並把新的筆記內容放在 Request Body 中傳給 Server。

例如 Client 可以傳送:

{
  "title": "學習 Node.js",
  "content": "今天練習 CRUD"
}

不過這裡有一個問題:Client 傳給 Server 的 JSON,並不是可以直接操作的 JavaScript 物件,而是放在 HTTP Request Body 裡傳送過來的資料。因此 Server 收到之後,需要先把這段 JSON 解析成 JavaScript 可以操作的資料。

在 Express 中,可以使用內建的 Middleware express.json() 來處理這件事:

app.use(express.json());

express.json() 會讀取 JSON 格式的 Request Body,解析完成後,再把結果放到 req.body 裡,讓後面的 Route 可以直接使用。

整個流程可以理解成:

Client 傳送 JSON
        ↓
JSON 放進 HTTP Request Body
        ↓
express.json() 解析 Request Body
        ↓
解析結果放進 req.body
        ↓
Route 使用 req.body

因為 express.json() 必須先處理 Request,所以通常會寫在建立 app 之後、所有需要讀取 JSON Body 的 Route 之前:

const express = require("express");

const app = express();

app.use(express.json());

app.post("/notes", function (req, res) {
  console.log(req.body);
});

app.listen(3000, function () {
  console.log("Server running at http://localhost:3000");
});

Express 會按照程式碼的順序處理 Request,所以當 POST /notes 的 Request 進來時,會先經過:

app.use(express.json());

等 Request Body 被解析完成之後,才會進到後面的 POST /notes Route。這時就可以透過 req.body 取得 Client 傳進來的資料。

例如 Client 傳入:

{
  "title": "學習 Node.js",
  "content": "今天練習 CRUD"
}

解析完成後,req.body 就會是:

{
  title: "學習 Node.js",
  content: "今天練習 CRUD"
}

因此就可以透過:

req.body.title
req.body.content

分別取得標題與內容。

接著建立 POST /notes

app.post("/notes", function (req, res) {
  const newNote = {
    id: notes.length + 1,
    title: req.body.title,
    content: req.body.content
  };

  notes.push(newNote);

  res.status(201).json({
    data: newNote
  });
});

這裡先從 req.body 取得 Client 傳入的資料,再建立新的 newNote

const newNote = {
  id: notes.length + 1,
  title: req.body.title,
  content: req.body.content
};

接著使用 push() 把新的 Note 加入 notes 陣列:

notes.push(newNote);

最後回傳:

res.status(201).json({
  data: newNote
});

這裡使用的是 201 Created。和一般成功時常看到的 200 OK 不同,201 更明確地表示這次 Request 成功建立了一個新的資源,因此很適合用在 POST 新增資料的情境。

這次 POST /notes 的流程可以整理成:

POST /notes
    ↓
Client 在 Request Body 傳入 JSON
    ↓
express.json() 解析 JSON
    ↓
解析結果放進 req.body
    ↓
透過 req.body 取得資料
    ↓
建立 newNote
    ↓
notes.push(newNote)
    ↓
回傳 201 Created

這裡也可以順便和前面的 req.params 做區分:req.params 用來取得 URL 中的動態參數,而 req.body 則用來取得 Request Body 中的資料。

建立 PATCH /notes/:id

接著實作 Update。假設現在要修改 ID 為 1 的筆記,可以使用:

PATCH /notes/1

並在 Request Body 中只傳入需要修改的欄位:

{
  "title": "重新學習 Express"
}

Route 可以寫成:

app.patch("/notes/:id", function (req, res) {
  const id = Number(req.params.id);

  const note = notes.find(function (note) {
    return note.id === id;
  });

  if (!note) {
    return res.status(404).json({
      message: "Note not found"
    });
  }

  if (req.body.title !== undefined) {
    note.title = req.body.title;
  }

  if (req.body.content !== undefined) {
    note.content = req.body.content;
  }

  res.status(200).json({
    data: note
  });
});

這裡會先透過 req.params.id 找出要修改哪一篇 Note,再透過 req.body 取得新的內容。

假設原本資料是:

{
  id: 1,
  title: "學習 Express",
  content: "今天開始學習 Express CRUD"
}

Client 只傳入:

{
  "title": "Express CRUD 練習"
}

更新後就會變成:

{
  "id": 1,
  "title": "Express CRUD 練習",
  "content": "今天開始學習 Express CRUD"
}

因為這次沒有傳入 content,所以原本的內容會保留下來。

PATCH 和 PUT 有什麼不同?

更新資料時,常看到 PATCHPUT 兩種 HTTP Method。兩者都可以用來修改資料,但概念上有些不同。

PATCH 通常用來表示部分更新,例如只修改 title:

{
  "title": "新的標題"
}

PUT 通常表示用新的資料完整取代原本的資源。

在實際專案中,不同團隊可能會有自己的 API 規範,不過這次 Notes API 只需要修改部分欄位,因此使用 PATCH

建立 DELETE /notes/:id

最後來實作 Delete。如果 Client 想刪除 ID 為 2 的 Note,可以發送:

DELETE /notes/2

可以先使用 findIndex() 找出資料所在的位置,再透過 splice() 把它從陣列中刪除。

app.delete("/notes/:id", function (req, res) {
  const id = Number(req.params.id);

  const noteIndex = notes.findIndex(function (note) {
    return note.id === id;
  });

  if (noteIndex === -1) {
    return res.status(404).json({
      message: "Note not found"
    });
  }

  notes.splice(noteIndex, 1);

  res.status(200).json({
    message: "Note deleted successfully"
  });
});

findIndex() 找到資料時會回傳它在陣列中的位置,如果找不到則會回傳 -1,所以我們可以利用這個結果判斷資料是否存在。

找到資料之後:

notes.splice(noteIndex, 1);

代表從 noteIndex 這個位置開始,刪除一筆資料。

完整 CRUD 程式碼

把前面的內容整理在一起,完整的 Express CRUD API 如下:

const express = require("express");

const app = express();

app.use(express.json());

let notes = [
  {
    id: 1,
    title: "學習 Express",
    content: "今天開始學習 Express CRUD"
  },
  {
    id: 2,
    title: "學習 RESTful API",
    content: "理解 HTTP Method 和 API 的關係"
  }
];

app.get("/notes", function (req, res) {
  res.status(200).json({
    data: notes
  });
});

app.get("/notes/:id", function (req, res) {
  const id = Number(req.params.id);

  const note = notes.find(function (note) {
    return note.id === id;
  });

  if (!note) {
    return res.status(404).json({
      message: "Note not found"
    });
  }

  res.status(200).json({
    data: note
  });
});

app.post("/notes", function (req, res) {
  const newNote = {
    id: notes.length + 1,
    title: req.body.title,
    content: req.body.content
  };

  notes.push(newNote);

  res.status(201).json({
    data: newNote
  });
});

app.patch("/notes/:id", function (req, res) {
  const id = Number(req.params.id);

  const note = notes.find(function (note) {
    return note.id === id;
  });

  if (!note) {
    return res.status(404).json({
      message: "Note not found"
    });
  }

  if (req.body.title !== undefined) {
    note.title = req.body.title;
  }

  if (req.body.content !== undefined) {
    note.content = req.body.content;
  }

  res.status(200).json({
    data: note
  });
});

app.delete("/notes/:id", function (req, res) {
  const id = Number(req.params.id);

  const noteIndex = notes.findIndex(function (note) {
    return note.id === id;
  });

  if (noteIndex === -1) {
    return res.status(404).json({
      message: "Note not found"
    });
  }

  notes.splice(noteIndex, 1);

  res.status(200).json({
    message: "Note deleted successfully"
  });
});

app.listen(3000, function () {
  console.log("Server running at http://localhost:3000");
});

到這裡,我們就完成了第一組完整的 CRUD API。

基本 Status Code

這次實作主要使用了三個常見的 HTTP Status Code:

Status Code 名稱 使用時機
200 OK Request 成功
201 Created 成功建立新的資源
404 Not Found 找不到指定資源

例如 GET /notes 成功取得資料時,可以回傳 200 OKPOST /notes 成功建立新的 Note 時,可以回傳 201 Created;而 GET /notes/999 找不到對應資料時,就可以回傳 404 Not Found

之後還會遇到更多 Status Code,例如 400 Bad Request401 Unauthorized403 Forbidden500 Internal Server Error,目前先掌握這三個最基本的即可。

用 Postman 測試完整 CRUD

API 寫完之後,可以使用 Postman 實際把 CRUD 流程走過一次。

取得所有 Notes

Method 選擇 GET,URL 輸入:

http://localhost:3000/notes

送出後應該會看到目前所有 Notes。

取得單一 Note

Method 選擇 GET,URL 輸入:

http://localhost:3000/notes/1

送出後應該會看到 ID 為 1 的 Note。

也可以測試不存在的 ID:

http://localhost:3000/notes/999

這時應該會收到 404 Not Found

新增 Note

Method 選擇 POST,URL 輸入:

http://localhost:3000/notes

接著在 Body 選擇 raw,格式選擇 JSON,再輸入:

{
  "title": "我的第三篇筆記",
  "content": "今天完成第一組 CRUD API"
}

送出後應該會收到 201 Created,並看到剛剛建立的新 Note。

修改 Note

Method 選擇 PATCH,URL 輸入:

http://localhost:3000/notes/1

Body 輸入:

{
  "title": "修改後的筆記標題"
}

送出後,可以再使用 GET /notes/1 確認資料是否真的被修改。

刪除 Note

Method 選擇 DELETE,URL 輸入:

http://localhost:3000/notes/2

成功刪除後,再執行:

GET /notes

就可以確認 ID 為 2 的 Note 是否已經從資料中消失。

從一次測試看懂 CRUD

如果把剛才的操作按照順序跑一次,其實就是一筆資料最基本的生命週期:

POST /notes
建立資料

GET /notes
查看資料

PATCH /notes/:id
修改資料

GET /notes/:id
確認修改結果

DELETE /notes/:id
刪除資料

這就是 CRUD 最核心的概念。

之後不管遇到文章、會員、商品、留言或訂單等功能,都很常看到類似的 API 結構。例如文章可能會有 GET /articlesPOST /articlesPATCH /articles/:idDELETE /articles/:id,雖然操作的資源不同,但背後的設計邏輯其實非常接近。

小結

這一篇我們完成了第一組完整的 Notes CRUD API,包含取得全部 Notes、取得單一 Note、新增 Note、修改 Note 和刪除 Note。

目前完成的 API 如下:

GET    /notes
GET    /notes/:id
POST   /notes
PATCH  /notes/:id
DELETE /notes/:id

過程中也認識了 req.paramsreq.bodyexpress.json()req.params 用來取得 URL 中的動態參數,req.body 用來取得 Client 放在 Request Body 中的資料,而 express.json() 則負責解析 JSON 格式的 Request Body。

目前 Notes 還只是存放在 JavaScript 陣列中,所以 Server 一旦重新啟動,資料就會消失。不過在進入資料庫之前,先把 CRUD、HTTP Method 與 RESTful API 之間的關係理解清楚,之後把資料來源換成 PostgreSQL 或其他資料庫時,API 的基本設計邏輯仍然會延續下去。


上一篇
Day 05|從原生 Node.js 到 Express:不用再自己處理每個細節
下一篇
Day 07|CRUD 寫完後:搞懂 Params、Query、Body,並修正 ID
系列文
出發吧!後端菜鳥:30 天的後端學習紀錄10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言